package cn.anecansaitin.jep;

import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;

public class Lexer {
    private final String source;
    private final int length;
    private int current = 0;
    private int line = 1;
    private boolean hasError;
    private final List<String[]> errors = new ArrayList<>();
    private List<Token> tokens = new ArrayList<>();
    private final List<Integer> lineStarts = new ArrayList<>();

    public Lexer(String source) {
        this.source = source;
        this.length = source.length();
        lineStarts.add(0);
    }

    public void analyze() {
        if (source.isBlank()) {
            tokens.add(Token.eof(0, 0));
            return;
        }

        while (hasChar()) {
            switch (consume()) {
                case '\t', '\u000B', '\f', '\u001C', '\u001D', '\u001E', '\u001F', ' ' -> {
                }
                case '\r', '\n' -> nextLine();
                case '+' -> tokens.add(readPlusOperators());
                case '-' -> tokens.add(readMinusOperators());
                case '*' -> tokens.add(readMultiplyOperators());
                case '/' -> readDivideOperators(tokens);
                case '%' -> tokens.add(readModuloOperators());
                case '&' -> tokens.add(readBitAndOperators());
                case '^' -> tokens.add(readBitXOrOperators());
                case '|' -> tokens.add(readBitOrOperators());
                case '~' -> tokens.add(Token.bitNot(line, getColumn(current - 1)));
                case '<' -> tokens.add(readLessThenOperators());
                case '>' -> tokens.add(readGreaterThenOperators());
                case '=' -> tokens.add(readEqualOperators());
                case '!' -> tokens.add(readNotOperators());
                case '(' -> tokens.add(Token.leftParen(line, getColumn(current - 1)));
                case ')' -> tokens.add(Token.rightParen(line, getColumn(current - 1)));
                case '[' -> tokens.add(Token.leftBracket(line, getColumn(current - 1)));
                case ']' -> tokens.add(Token.rightBracket(line, getColumn(current - 1)));
                case '{' -> tokens.add(Token.leftBrace(line, getColumn(current - 1)));
                case '}' -> tokens.add(Token.rightBrace(line, getColumn(current - 1)));
                case ',' -> tokens.add(Token.comma(line, getColumn(current - 1)));
                case ';' -> tokens.add(Token.semicolon(line, getColumn(current - 1)));
                case '.' -> tokens.add(readDotOrNumber());
                case ':' -> tokens.add(Token.colon(line, getColumn(current - 1)));
                case '?' -> tokens.add(Token.question(line, getColumn(current - 1)));
                case '\'' -> tokens.add(readCharLiteral());
                case '"' -> tokens.add(readStringLiteral());
                case 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
                     'u', 'v', 'w', 'x', 'y', 'z',
                     'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
                     'U', 'V', 'W', 'X', 'Y', 'Z',
                     '_' -> tokens.add(readKeywordIdentifierSpecial());
                case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> tokens.add(readNumber(false));
                default -> {
                    markError("Illegal char");
                    tokens.add(Token.error(line, getColumn(current - 1)));
                }
            }
        }

        tokens.add(Token.eof(line, getColumn(current - 1)));
    }

    private Token readPlusOperators() {
        switch (peek()) {
            case '+' -> {
                next();
                return Token.inc(line, getColumn(current - 2));
            }
            case '=' -> {
                next();
                return Token.assignPlus(line, getColumn(current - 2));
            }
            default -> {
                if (tokens.isEmpty()) {
                    return Token.unaryPlus(line, getColumn(current - 1));
                }

                if (tokens.getLast().type().canBeUnary()) {
                    return Token.unaryPlus(line, getColumn(current - 1));
                }

                return Token.plus(line, getColumn(current - 1));
            }
        }
    }

    private Token readMinusOperators() {
        switch (peek()) {
            case '-' -> {
                next();
                return Token.dec(line, getColumn(current - 2));
            }
            case '=' -> {
                next();
                return Token.assignMinus(line, getColumn(current - 2));
            }
            default -> {
                if (tokens.isEmpty()) {
                    return Token.unaryMinus(line, getColumn(current - 1));
                }

                if (tokens.getLast().type().canBeUnary()) {
                    return Token.unaryMinus(line, getColumn(current - 1));
                }

                return Token.minus(line, getColumn(current - 1));
            }
        }
    }

    private Token readMultiplyOperators() {
        switch (peek()) {
            case '*' -> {
                next();
                return Token.power(line, getColumn(current - 2));
            }
            case '=' -> {
                next();
                return Token.assignMultiply(line, getColumn(current - 2));
            }
            default -> {
                return Token.multiply(line, getColumn(current - 1));
            }
        }
    }

    private void readDivideOperators(List<Token> tokens) {
        switch (peek()) {
            case '=' -> {
                next();
                tokens.add(Token.assignDivide(line, getColumn(current - 2)));
            }
            case '/' -> moveToNextLine();
            case '*' -> {
                while (hasChar()) {
                    switch (consume()) {
                        case '*' -> {
                            if (peek() == '/') {
                                next();
                                return;
                            }
                        }
                        case '\r', '\n' -> nextLine();
                        default -> next();
                    }
                }
            }
            default -> tokens.add(Token.divide(line, getColumn(current - 1)));
        }
    }

    private Token readBitAndOperators() {
        switch (peek()) {
            case '&' -> {
                next();
                return Token.and(line, getColumn(current - 2));
            }
            case '=' -> {
                next();
                return Token.assignBitAnd(line, getColumn(current - 2));
            }
            default -> {
                return Token.bitAnd(line, getColumn(current - 1));
            }
        }
    }

    private Token readBitXOrOperators() {
        if (peek() == '=') {
            next();
            return Token.assignBitXor(line, getColumn(current - 2));
        } else {
            return Token.bitXor(line, getColumn(current - 1));
        }
    }

    private Token readBitOrOperators() {
        switch (peek()) {
            case '|' -> {
                next();
                return Token.or(line, getColumn(current - 2));
            }
            case '=' -> {
                next();
                return Token.assignBitOr(line, getColumn(current - 2));
            }
            default -> {
                return Token.bitOr(line, getColumn(current - 1));
            }
        }
    }

    private Token readModuloOperators() {
        if (peek() == '=') {
            next();
            return Token.assignModulo(line, getColumn(current - 2));
        } else {
            return Token.modulo(line, getColumn(current - 1));
        }
    }

    private Token readLessThenOperators() {
        switch (peekCharacter()) {
            case '=' -> {
                next();
                return Token.lessThanEqual(line, getColumn(current - 2));
            }
            case '<' -> {
                next();

                if (peek() == '=') {
                    next();
                    return Token.assignBitLeft(line, getColumn(current - 3));
                }

                return Token.bitLeft(line, getColumn(current - 2));
            }
            case null, default -> {
                return Token.lessThan(line, getColumn(current - 1));
            }
        }
    }

    private Token readGreaterThenOperators() {
        switch (peekCharacter()) {
            case '=' -> {
                next();
                return Token.greaterThanEqual(line, getColumn(current - 2));
            }
            case '>' -> {
                next();

                switch (peek()) {
                    case '=' -> {
                        next();
                        return Token.assignBitRight(line, getColumn(current - 3));
                    }
                    case '>' -> {
                        next();
                        return Token.bitRightUnsigned(line, getColumn(current - 3));
                    }
                    default -> {
                        return Token.bitRight(line, getColumn(current - 2));
                    }
                }
            }
            case null, default -> {
                return Token.greaterThan(line, getColumn(current - 1));
            }
        }
    }

    private Token readEqualOperators() {
        if (peek() == '=') {
            next();
            return Token.equal(line, getColumn(current - 2));
        }

        return Token.assign(line, getColumn(current - 1));
    }

    private Token readNotOperators() {
        if (peek() == '=') {
            next();
            return Token.notEqual(line, getColumn(current - 2));
        }

        return Token.not(line, getColumn(current - 1));
    }

    private Token readDotOrNumber() {
        switch (peek()) {
            case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> {
                return readNumber(true);
            }
            default -> {
                return Token.dot(line, getColumn(current - 1));
            }
        }
    }

    private Token readNumber(boolean isDot) {
        int start = current - 1;
        boolean hasPoint = isDot;
        boolean hasExponent = false;

        loop:
        while (hasChar()) {
            char c = unsafePeek();

            if (!hasExponent) {
                // 无科学计数部分
                switch (c) {
                    case '.' -> {
                        if (hasPoint) {
                            next();
                            markError("Illegal number: multiple decimal points");
                            return Token.error(line, getColumn(start));
                        } else {
                            hasPoint = true;
                            next();
                        }
                    }
                    case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> next();
                    case 'E', 'e' -> {
                        hasExponent = true;
                        next();
                    }
                    case 'f', 'F' -> {
                        next();
                        return Token.floatLiteral(source.substring(start, current - 1), line, getColumn(start));
                    }
                    case 'd', 'D' -> {
                        next();
                        return Token.doubleLiteral(source.substring(start, current - 1), line, getColumn(start));
                    }
                    case 'l', 'L' -> {
                        next();
                        return Token.longLiteral(source.substring(start, current - 1), line, getColumn(start));
                    }
                    default -> {
                        break loop;
                    }
                }
            } else {
                // 科学计数
                switch (c) {
                    case '+', '-' -> {
                        char pre = source.charAt(current - 1);

                        if (pre != 'E' && pre != 'e') {
                            markError("Illegal scientific notation: sign must follow 'e' or 'E'");
                            next();
                            return Token.error(line, getColumn(start));
                        }

                        next();
                    }
                    case 'E', 'e' -> {
                        next();
                        markError("Illegal scientific notation: duplicate exponent symbol");
                        return Token.error(line, getColumn(start));
                    }
                    case 'f', 'F' -> {
                        char pre = source.charAt(current - 1);

                        if (!Character.isDigit(pre)) {
                            markError("Illegal scientific notation: missing digits in exponent");
                        }

                        next();
                        return Token.floatLiteral(source.substring(start, current - 1), line, getColumn(start));
                    }
                    case 'd', 'D' -> {
                        char pre = source.charAt(current - 1);

                        if (!Character.isDigit(pre)) {
                            markError("Illegal scientific notation: missing digits in exponent");
                        }

                        next();
                        return Token.doubleLiteral(source.substring(start, current - 1), line, getColumn(start));
                    }
                    case 'l', 'L' -> {
                        char pre = source.charAt(current - 1);

                        if (!Character.isDigit(pre)) {
                            markError("Illegal scientific notation: missing digits in exponent");
                        }

                        next();
                        return Token.longLiteral(source.substring(start, current - 1), line, getColumn(start));
                    }
                    case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> next();
                    default -> {
                        break loop;
                    }
                }
            }
        }

        if (hasExponent && !Character.isDigit(source.charAt(current - 1))) {
            markError("Illegal scientific notation: missing digits in exponent");
            return Token.error(line, getColumn(start));
        }

        return hasPoint ? Token.doubleLiteral(source.substring(start, current), line, getColumn(start)) : Token.intLiteral(source.substring(start, current), line, getColumn(start));
    }

    private Token readKeywordIdentifierSpecial() {
        int start = current - 1;

        loop:
        while (hasChar()) {
            char c = unsafePeek();

            switch (c) {
                case 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
                     'u', 'v', 'w', 'x', 'y', 'z',
                     'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
                     'U', 'V', 'W', 'X', 'Y', 'Z',
                     '1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
                     '_' -> next();
                default -> {
                    break loop;
                }
            }
        }

        String value = source.substring(start, current);
        int column = getColumn(start);
        Token keyword = Token.keyword(value, line, column);

        if (keyword != null) {
            return keyword;
        }

        Token innerLiteral = Token.innerLiteral(value, line, column);

        if (innerLiteral != null) {
            return innerLiteral;
        }

        char next = peek();

        if (next == '(') {
            return Token.function(value, line, column);
        }

        return Token.identifier(value, line, column);
    }

    private Token readCharLiteral() {
        int start = current;

        switch (peekCharacter()) {
            case '\\' -> {
                // 转义字符
                next();
                String result = readEscapeSequence();
                // 找到下一个单引号
                while (hasChar()) {
                    char c = unsafePeek();

                    switch (c) {
                        case '\'' -> {
                            next();
                            return result == null ? Token.error(line, getColumn(start - 1)) : Token.charLiteral(result, line, getColumn(start - 1));
                        }
                        case '\n', '\r' -> {
                            markError("Illegal character literal: missing closing quote");
                            moveToNextLine();
                            return Token.error(line, getColumn(start - 1));
                        }
                        default -> next();
                    }
                }

                markError("Illegal character literal: missing closing quote");
                return Token.error(line, getColumn(start - 1));
            }
            case '\'' -> {
                // 空字符
                next();
                markError("Illegal character literal: empty character literal");
                return Token.error(line, getColumn(start - 1));
            }
            case null -> {
                markError("Illegal character literal: missing closing quote");
                return Token.error(line, getColumn(start - 1));
            }
            default -> {
                // 普通字符
                next();

                switch (peekCharacter()) {
                    case '\'' ->{
                        // 字符闭合
                        next();
                        return Token.charLiteral(source.substring(start, current - 1), line, getColumn(start - 1));
                    }
                    case null -> {
                        markError("Illegal character literal: missing closing quote");
                        return Token.error(line, getColumn(start - 1));
                    }
                    default -> {
                        while (hasChar()) {
                            switch (unsafePeek()) {
                                case '\'' -> {
                                    markError("Illegal character literal: character literal too long");
                                    next();
                                    return Token.error(line, getColumn(start - 1));
                                }
                                case '\n', '\r' -> {
                                    markError("Illegal character literal: missing closing quote");
                                    moveToNextLine();
                                    return Token.error(line, getColumn(start - 1));
                                }
                                default -> next();
                            }
                        }

                        markError("Illegal character literal: missing closing quote");
                        return Token.error(line, getColumn(start - 1));
                    }
                }
            }
        }
    }

    private Token readStringLiteral() {
        int start = current - 1;
        StringBuilder result = new StringBuilder();

        while (hasChar()) {
            char c = unsafePeek();

            switch (c) {
                case '\\' -> {
                    next();
                    String escapeSequence = readEscapeSequence();

                    if (escapeSequence == null) {
                        return Token.error(line, getColumn(start));
                    }

                    result.append(escapeSequence);
                }
                case '\n', '\r' -> {
                    markError("Illegal string literal: missing closing quote");
                    return Token.error(line, getColumn(start));
                }
                case '"' -> {
                    next();
                    return Token.stringLiteral(result.toString(), line, getColumn(start));
                }
                default -> {
                    next();
                    result.append(c);
                }
            }
        }

        markError("Illegal string literal: missing closing quote");
        return Token.error(line, getColumn(start));
    }

    @Nullable
    private String readEscapeSequence() {
        int start = current - 1;

        switch (peekCharacter()) {
            case '0', '1', '2', '3' -> {
                // 八进制
                next();

                switch (peekCharacter()) {
                    case '0', '1', '2', '3', '4', '5', '6', '7' -> {
                        next();
                        Character c3 = peekCharacter();

                        switch (c3) {
                            case '0', '1', '2', '3', '4', '5', '6', '7' -> next();
                            case null, default -> {
                                markError("Illegal character: invalid escape sequence");
                                return null;
                            }
                        }
                    }
                    case null, default -> {
                        markError("Illegal character: invalid escape sequence");
                        return null;
                    }
                }

                return String.valueOf((char) Integer.parseInt(source.substring(start + 1, current), 8));
            }
            case 'u' -> {
                // 十六进制Unicode
                start++;
                next();

                for (int i = 0; i < 4; i++) {
                    Character c = peekCharacter();

                    switch (c) {
                        case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                             'a', 'b', 'c', 'd', 'e', 'f',
                             'A', 'B', 'C', 'D', 'E', 'F' -> next();
                        case null, default -> {
                            markError("Illegal character: invalid hexadecimal Unicode escape sequence");
                            return null;
                        }
                    }
                }

                return String.valueOf((char) Integer.parseInt(source.substring(start + 1, current), 16));
            }
            case 'b' -> {
                next();
                return "\b";
            }
            case 't' -> {
                next();
                return "\t";
            }
            case 'n' -> {
                next();
                return "\n";
            }
            case 'r' -> {
                next();
                return "\r";
            }
            case 'f' -> {
                next();
                return "\f";
            }
            case '"' -> {
                next();
                return "\"";
            }
            case '\'' -> {
                next();
                return "'";
            }
            case '\\' -> {
                next();
                return "\\";
            }
            case null, default -> {
                markError("Illegal character: invalid escape sequence");
                return null;
            }
        }
    }

    private void markError(String message) {
        hasError = true;
        String[] error = new String[3];
        int lineStart = lineStarts.get(line - 1);
        int lineEnd = length;
        error[0] = "[" + line + ":" + (current - lineStart + 1) + "]" + (message.isBlank() ? "Analyzing error" : message);

        for (int i = lineStart; i < length; i++) {
            if (source.charAt(i) == '\n') {
                lineEnd = i;
                break;
            }
        }

        error[1] = source.substring(lineStart, lineEnd);
        error[2] = " ".repeat(calculateVisualLength(current) - calculateVisualLength(lineStart)) + "^";
        errors.add(error);
    }

    private int calculateVisualLength(int position) {
        int visualLength = 0;

        for (int i = 0; i < position; i++) {
            if (source.charAt(i) == '\t') {
                visualLength = (visualLength / 8 + 1) * 8;
            } else {
                visualLength++;
            }
        }

        return visualLength;
    }

    private boolean hasChar() {
        return current < length;
    }

    private char unsafePeek() {
        return source.charAt(current);
    }

    @Nullable
    private Character peekCharacter() {
        return hasChar() ? source.charAt(current) : null;
    }

    private char peek() {
        return hasChar() ? source.charAt(current) : '\0';
    }

    private void next() {
        current++;
    }

    private char consume() {
        return source.charAt(current++);
    }

    private void moveToNextLine() {
        boolean notNextLine = true;

        while (notNextLine && hasChar()) {
            char c = unsafePeek();
            next();

            if (c == '\n' || c == '\r') {
                nextLine();
                notNextLine = false;
            }
        }
    }

    private void nextLine() {
        line++;
        lineStarts.add(current);
    }
    
    private int getColumn(int index) {
        int lineStart = lineStarts.get(line - 1);
        return index - lineStart + 1;
    }

    public boolean hasError() {
        return hasError;
    }

    public List<String[]> getErrors() {
        return errors;
    }

    public List<Token> getTokens() {
        return tokens;
    }
}
